#!/usr/bin/perl
# $Id: //depot/prod/vsa/Rrr1/vsautil/src/dvadmin#19 $

use strict;
use warnings;

# Add directory containing this executable to Perl path.
# Makes running this from source tree work correctly.
use lib($0 =~ /(^.*)\//);

# Prior to libwww-perl-5.837, the default was to ignore SSL certificates.
# The default behavior in libwww-perl-5.837 is to validate the SSL certificate.
# Since dvadmin is not an internet facing utility, it is safe to ignore SSL certificate errors.
$ENV{'PERL_LWP_SSL_VERIFY_HOSTNAME'} = 0;

my $usageStr = <<END_USAGE;
usage: dvadmin [<options>] [<command>]
    where <options> is a combination of:
	-s/--server <vSphere_server> ESX host or vCenter server managing Data ONTAP-v VM
	-h/--host <ESX_host>	     ESX host running Data ONTAP-v VM if diff from server
	-u/--user <vSphere_user>     vSphere administrator username on server
	-p/--password [password]     [prompt for] password for <vSphere_user>
	-e/--escape <escapeKey>      Console escape key; prefix control keys with ^
	-q/--quiet		     run quietly
	--save			     save server/host/user/escape key for next time
END_USAGE

use VMware::VIRuntime;
use VMware::VILib;
use VMware::VICredStore;

use Getopt::Long;
use POSIX;
use Socket;
use Term::ReadLine;
use File::Temp qw/ tempdir /;

use DVADMIN::Config;
use DVADMIN::Util;
use DVADMIN::VM;
use DVADMIN::Host;
use DVADMIN::POOL;
use DVADMIN::PDISK;
use DVADMIN::NETWORK;
use DVADMIN::Interpreter;
use DVADMIN::Monitor;
use DVADMIN::ASUP;
use DVADMIN::Wizard;

use constant BANNER =>
    "NetApp Data ONTAP-v Administration Tool " . Config::VERSION . "\n" .
    "Copyright (C) 1992-2011 NetApp.\n" .
    "All rights reserved.\n";

# cmdline options
my ($server, $hostnameInCmdLine, $user, $password, $save, $quiet, $debug);
my $consoleEscapeKey = chr(29);	# default console escape key: Ctrl-] aka ^]

# other globals
my $vim;
my $apiType;
my $pterm;
my $consoleVmName;
my $consoleEscapeKeyPrintable;
my $term;
my $save_vsphere_password;
my $cred_initialized = 0;
my $hostnameInRcFile;

sub usage {
    my ($msg) = @_;
    Util::msg($msg . "\n\n") if (defined $msg);
    Util::msg($usageStr);
    exit 1;
}

$pterm = POSIX::Termios->new();
$pterm->getattr(0);
my $initialLflag = $pterm->getlflag();

sub input_mode_reset_flags($) {
    my ($flags) = (@_);
    $pterm->setlflag($initialLflag & ~$flags);
    $pterm->setattr(0, POSIX::TCSANOW);
}

sub input_mode_set_flags($) {
    my ($flags) = (@_);
    $pterm->setlflag($initialLflag | $flags);
    $pterm->setattr(0, POSIX::TCSANOW);
}

sub input_mode_normal() {
    $pterm->setlflag($initialLflag);
    $pterm->setattr(0, POSIX::TCSANOW);
}

sub getpass($) {
    my ($prompt) = @_;

    print $prompt;

    input_mode_reset_flags(POSIX::ECHO | POSIX::ECHOK | POSIX::ICANON);
    my $password = <STDIN>;
    input_mode_normal();
    print "\n";
    chop $password;

    return $password;
}

sub get_force_opt {
    my ($args) = @_;
    return (@$args > 0 && $$args[0] eq "--force") ? shift(@$args) : undef;
}

# Return the memory size for the VSA.  If --memsize=<size> is on
# the command line, make sure a valid size is specified and return it.
# If --memsize=<size> is omitted from the command line, return 4G (the default size).
sub get_memsize_opt {
    my ($args) = @_;
    if (@$args > 0 && $$args[0] =~ "--memsize") {
	my ($key, $size) = split(/=/, $$args[0]);
        shift(@$args);
        $size = uc($size);
        ($size eq "4G" || $size eq "32G" || $size eq "96G")
	  or die new InvalidArg("<memory size>", "Must be 4G, 32G, or 96G");
        return $size;
    }
    # If no --memsize specified, do not declare a default size to allow
    # use of default deployment option in ovf file.
}

# Check if headswap is specified or not
sub get_headswap_opt {
    my ($args) = @_;
    my $index = 0;
    while ($index < @$args) {
	if ($$args[$index] eq "--headswap") {
	    splice(@$args, $index, 1);
	    return 1;
	} elsif ($$args[$index] =~ /--headswap/) {
	    die new InvalidArg("headswap", "no value is expected.");
	}
	$index++;
    }
    return 0;
}

sub noVmNamed($) {
    my ($vmName) = @_;
    return new InvalidArg("<vm_name>", "no VM named '$vmName'");
}

sub isCommand($;$) {
    my ($interp, $word) = @_;
    eval {
	$interp->lookup($word);
    };
    return $@ ? 0 : 1;
}

sub currentHostname($) {
    my ($interp) = @_;
    my $hostname = $interp->getenv("hostname")
	or die "'hostname' not set in interpreter environment";
    return $hostname;
}

sub currentHostSys($;@) {
    my ($interp, @props) = @_;
    return Host::get(currentHostname($interp), @props);
}

sub cmd_host_ssh_setup {
    my ($interp, $args) = @_;
    my $hostname = currentHostname($interp);
    my $passwd = shift(@$args);
    die new UsageErr unless (@$args == 0);

    Host::sshCheckModules();

    if (defined $passwd) {
	# Since ssh password specified, try non-interactively.
	Host::sshSendKey($hostname, $passwd, 0);
    } else {
	# No ssh password specified, so try vSphere password, falling
	# back to prompt user if that fails and session allows it.
	Host::sshSendKey($hostname, $password, is_interactive());
    }
    # Test non-interactive connect
    my $stat = Host::sshStatusRefresh($hostname, $password);
    Util::msg("Non-interactive SSH to '$hostname': $stat");
}

sub cmd_host_ssh_status {
    my ($interp, $args) = @_;
    my $hostname = currentHostname($interp);
    die new UsageErr unless (@$args == 0);
    Host::sshCheckModules();
    my $stat = Host::sshStatusRefresh($hostname, $password);
    Util::msg("Non-interactive SSH to '$hostname': $stat");
}

sub cmd_vm {
    my ($interp, $args) = @_;
    if (@$args == 1 && !isCommand($interp, $$args[0])) {
	my $vmName = shift(@$args);
	my $vm = VM::find($vmName)
	    or die noVmNamed($vmName);
	$interp->setenv("vm_name", $vmName);
	$interp->setenv("prompt_extra", $vmName);
    }
    return Interpreter::STATUS_OK;
}

sub get_vm_name {
    my ($interp, $args) = @_;
    my $vm_name = $interp->getenv("vm_name");
    return $vm_name ? $vm_name : shift(@$args);
}

#Starts the wizard for creating a VM
sub cmd_vm_wizard {
    my $interp = shift @_;

    #save current signal handler for later
    my $prev_sig_int = $SIG{"INT"};

    Util::msg("Welcome to the VM create wizard!");
    Util::msg("This wizard will take you step-by-step through the process of creating a Data ONTAP virtual machine.");
    Util::msg("Use ^C at any prompt to exit the wizard.");

    my @args = ();
    my @oldargs = ();

    my $host = currentHostSys($interp);

    my $vm_name;
    my $vm_pool;

    #get all arguments needed to create the VM
    eval{
        do {
            #save args from previous iteration, and reset args
            @oldargs = @args;
            @args = ();
            Wizard::get_main_args($host, \@args, \@oldargs);

            #save the VM name for later
            $vm_name = $args[0];
            Wizard::get_additional_args(\@args, \@oldargs, $pterm);

            Wizard::print_summary(\@args);
        } while(!(Wizard::user_confirm("Would you like to create a VM with these parameters?", 'y')));

        #create the VM
        Util::msg("Creating VM...");
        Wizard::run_dvadmin_command($interp, \@args, \&cmd_vm_create);

        #reinit args for next step
        @args = ($vm_name);

        #ask user if they would like to set additional arguments
        Wizard::get_optional_args($interp, $host, \@args, \&cmd_vm_network_connect);

        #only call command if user chose at least one optional parameter to set
        if (@args > 1) {
            Util::msg("You have set the following parameters:");
            Wizard::run_dvadmin_command($interp, \@args, \&cmd_vm_prop_set);
        }

        #ask user if they would like to create a data disk
        if (Wizard::user_confirm("Would you like to the define the data disks that the VM will manage?", 'y')) {
            Wizard::create_disks($interp, $host, $vm_name, \&cmd_vm_disk_create);
        }

        @args = ("--no-watchdog", $vm_name);
        if (Wizard::user_confirm("Would you like to enable automatic backup of your VM's configuration?", 'y')) {
            Wizard::run_dvadmin_command($interp, \@args, \&cmd_vm_monitor_start);
        }

        @args = ($vm_name);
        if (Wizard::user_confirm("Your VM has been configured.  Would you like to see a summary?", 'y')) {
            Wizard::run_dvadmin_command($interp, \@args, \&cmd_vm_show);
        }

        @args = ($vm_name);
        if (Wizard::user_confirm("Would you like to start the VM?", 'y')) {
            Wizard::run_dvadmin_command($interp, \@args, \&cmd_vm_start);
        }
    };

    if ($@) {
        return Wizard::exit_wizard($prev_sig_int);
    }
    Util::msg("VM wizard is now complete!  You can connect to your VM by typing 'vm console connect $vm_name', or type 'help' to view the available commands.");

    #restore signal handler
    $SIG{"INT"} = $prev_sig_int;

    return Interpreter::STATUS_OK;
}


# The minimum number of arguments to 'vm create' is five, excluding
# the vm name and flags.  This includes:
# <pool_name> <nv_pool_name> <network_name> <ovf_pkg> and then
# a minimum of one property when specifying the license for a
# headswap procedure.
use constant VM_CREATE_MIN_ARGS => 5;


sub cmd_vm_create {
    my ($interp, $args) = @_;

    die new UsageErr if (@$args == 0);

    my $headswap = get_headswap_opt($args);
    my $memsize = get_memsize_opt($args);
    my $vmName = get_vm_name($interp, $args)
	  and @$args >=  VM_CREATE_MIN_ARGS
	  or die new UsageErr;
    my $pool = shift(@$args);
    my $nvpool = shift(@$args);
    my $network = shift(@$args);
    my $ova = shift(@$args);
    if ($headswap != 1) {
	push(@$args, "raidtype=RAID0") unless grep(/^raidtype=/, @$args);
    }
    my $esxhost = currentHostname($interp);

    if (!grep(/^vsphere_hostname=/, @$args)) {
	# pass vsphere host info as IP address (DNS may not be enabled in the DOT VM)
	my $address = gethostbyname($esxhost);
	if ($? eq 0) { $address = inet_ntoa($address); }
	else { $address = $esxhost; }
	push(@$args, "vsphere_hostname=$address");
    }

    VM::create($vmName, $esxhost, currentHostSys($interp), $pool, $nvpool,
               $network, $ova, $memsize, $args, $headswap);
    Util::msg("Created VM $vmName on host $esxhost.\n");

    return Interpreter::STATUS_OK;
}

sub gdb_show($$$) {
    my ($vm, $showAlways, $hostname) = @_;
    my $port = VM::findSerial($vm, VM::VM_DEBUG_PORT)
	or return;
    my $vmName = $vm->name;
    my $backing = $port->backing;
    if ($backing &&
	$backing->isa("VirtualSerialPortURIBackingInfo") &&
	$backing->direction eq "server" &&
	$backing->serviceURI =~ m%^tcp://:(\d+)$% ) {
	Util::msg("  gdb connections to $vmName redirected to port $1 on $hostname\n");
    } elsif ($showAlways) {
	Util::msg("  gdb connection for $vmName stopped\n");
    }
}


sub cmd_vm_gdb_start {
    my ($interp, $args) = @_;
    my $vmName = get_vm_name($interp, $args)
	and my $tcpPort = shift(@$args)
	and @$args == 0
	or die new UsageErr;
    my $vm = VM::find($vmName)
	or die noVmNamed($vmName);
    VM::serialConfigAsNetServer(currentHostSys($interp), $vm, 1, $tcpPort);
    gdb_show($vm, 1, currentHostname($interp));
    return Interpreter::STATUS_OK;
}


sub cmd_vm_gdb_stop {
    my ($interp, $args) = @_;
    my $force = get_force_opt($args);
    my $vmName = get_vm_name($interp, $args)
	and @$args == 0
	or die new UsageErr;
    my $vm = VM::find($vmName)
	or die noVmNamed($vmName);
    VM::serialConfigAsPipe($vm, 1);
    gdb_show($vm, 1, currentHostname($interp));
    return Interpreter::STATUS_OK;
}


sub cmd_vm_gdb_show {
    my ($interp, $args) = @_;
    my $vmName = get_vm_name($interp, $args);
    @$args == 0	or die new UsageErr;
    if ($vmName) {
	my $vm = VM::find($vmName)
	    or die noVmNamed($vmName);
	gdb_show($vm, 1, currentHostname($interp));
    } else {
	my $host = currentHostSys($interp);
	my $vms = Vim::find_entity_views(begin_entity => $host,
					 view_type => "VirtualMachine");
	foreach my $vm (@$vms) {
	    gdb_show($vm, 0, currentHostname($interp));
	}
    }
    return Interpreter::STATUS_OK;
}

sub cmd_vm_coredump {
    my ($interp, $args) =@_;
    my $vmName = get_vm_name($interp, $args);
    die new UsageErr if (!$vmName);
    my $vm = VM::find($vmName)
	or die noVmNamed($vmName);
    die new UsageErr if (@$args > 1);
    VM::coredump(currentHostSys($interp), $vm, $server, $user, $password, $$args[0]);
    return Interpreter::STATUS_OK;
}


sub remove_sk_process_timeout_override {
    my ($vm) = @_;
    my $vmName = $vm->name;
    eval { VM::update_config_state($vm); };	# silently ignore errs
    my %properties = Util::readPropertyFile();
    if (%properties) {
	my $vmProps = $properties{$vmName};
	if ($vmProps) {
	    if ($$vmProps{"sk_process_timeout_override"}) {
		my $configState = $$vmProps{"config"};
		if ($configState && $configState eq "ready") {
		    Util::msg("You may need to disable hung process detection via ONTAP CLI:\n" .
			      "    options sk.process.timeout.override 0\n");
		} else {
		    delete $$vmProps{"sk_process_timeout_override"};
		    Util::writePropertyFile(%properties);
		    Util::msg("Disabled ONTAP hung process detection for '$vmName'\n");
		}
	    }
	}
    }
}


sub cmd_vm_reserve_cpu {
    my ($interp, $args) = @_;
    my $vmName = get_vm_name($interp, $args)
	and @$args < 2
	or die new UsageErr;
    my $vm = VM::find($vmName)
	or die noVmNamed($vmName);
    VM::reserveCPU(currentHostSys($interp), $vm, $$args[0]);
    remove_sk_process_timeout_override($vm);
    return Interpreter::STATUS_OK;
}

sub cmd_vm_reserve_memory {
    my ($interp, $args) = @_;
    my $vmName = get_vm_name($interp, $args)
	and @$args < 2
	or die new UsageErr;
    my $vm = VM::find($vmName)
	or die noVmNamed($vmName);
    VM::reserveMemory($vm, $$args[0]);
    remove_sk_process_timeout_override($vm);
    return Interpreter::STATUS_OK;
}

sub cmd_vm_htsharing {
    my ($interp, $args) = @_;
    my $vmName = get_vm_name($interp, $args)
	and @$args < 2 or die new UsageErr;
    my $vm = VM::find($vmName)
	or die noVmNamed($vmName);
    my $sharing = $$args[0];
    if (defined $sharing && ($sharing ne 'any' && $sharing ne "none" && $sharing ne "internal")) {
	die new UsageErr;
    }
    VM::setHTSharing(currentHostSys($interp, "vm"), $vm, $sharing);
    return Interpreter::STATUS_OK;
}

sub cmd_vm_affinity_cpu {
    my ($interp, $args) = @_;
    my $vmName = get_vm_name($interp, $args)
	or die new UsageErr;
    my $vm = VM::find($vmName)
	or die noVmNamed($vmName);
    VM::affinityCPU(currentHostSys($interp, "vm"), $vm, $args);
    return Interpreter::STATUS_OK;
}

sub cmd_vm_affinity_memory {
    my ($interp, $args) = @_;
    my $vmName = get_vm_name($interp, $args)
	or die new UsageErr;
    my $vm = VM::find($vmName)
	or die noVmNamed($vmName);
    VM::affinityMemory(currentHostSys($interp, "vm"), $vm, $args);
    return Interpreter::STATUS_OK;
}

sub cmd_vm_affinity_show {
    my ($interp, $args) = @_;
    my $vmName = get_vm_name($interp, $args);
    my $vm = VM::find($vmName);
    VM::affinityShow(currentHostSys($interp, "vm"), $vm);
    return Interpreter::STATUS_OK;
}

sub cmd_vm_uuid_show {
    my ($interp, $args) = @_;
    my $vmName = get_vm_name($interp, $args)
	and @$args == 0
	or die new UsageErr;
    my $vm = VM::find($vmName)
	or die noVmNamed($vmName);
    VM::showUUID($vm);
    return Interpreter::STATUS_OK;
}

sub cmd_vm_destroy {
    my ($interp, $args) = @_;
    my $vmName = get_vm_name($interp, $args)
	and @$args == 0
	or die new UsageErr;
    my $vm = VM::find($vmName)
	or die noVmNamed($vmName);
    VM::destroy(currentHostSys($interp), $vm);
    return Interpreter::STATUS_OK;
}

sub cmd_vm_show {
    my ($interp, $args) = @_;
    my $vmName = get_vm_name($interp, $args);
    @$args == 0 or die new UsageErr;
    my $vm;
    if ($vmName) {
	$vm = VM::find($vmName) or die noVmNamed($vmName);
    }
    VM::show(currentHostSys($interp, "vm"), $vm);
    return Interpreter::STATUS_OK;
}

sub cmd_vm_network_show {
    my ($interp, $args) = @_;
    my $vmName = get_vm_name($interp, $args)
	and @$args == 0
	or die new UsageErr;
    my $vm = VM::find($vmName) or die noVmNamed($vmName);
    VM::showNetwork($vm);
    return Interpreter::STATUS_OK;
}

sub cmd_vm_network_connect {
    my ($interp, $args) = @_;
    my $vmName = get_vm_name($interp, $args)
	and @$args == 2
	or die new UsageErr;
    my $vm = VM::find($vmName) or die noVmNamed($vmName);
    my $ifName = shift(@$args);
    my $netName = shift(@$args);
    VM::connectNetwork(currentHostSys($interp), $vm, $ifName, $netName);
    return Interpreter::STATUS_OK;
}

sub cmd_vm_disk_show {
    my ($interp, $args) = @_;
    my $vmName = get_vm_name($interp, $args)
	and @$args == 0
	or die new UsageErr;
    my $vm = VM::find($vmName) or die noVmNamed($vmName);
    VM::showDisk(currentHostSys($interp), $vm);
    return Interpreter::STATUS_OK;
}

sub cmd_vm_state_show {
    my ($interp, $args) = @_;
    my $vmName = get_vm_name($interp, $args)
	and @$args == 0
	or die new UsageErr;
    my $vm = VM::find($vmName) or die noVmNamed($vmName);
    VM::showState($vm);
    return Interpreter::STATUS_OK;
}

sub cmd_vm_disk_create {
    my ($interp, $args) = @_;
    my $vmName = get_vm_name($interp, $args)
	and @$args >= 1
	and @$args <= 2
	or die new UsageErr;
    my $vm = VM::find($vmName) or die noVmNamed($vmName);
    my $store = shift(@$args);
    my $size = shift(@$args);

    my $sizeInKb;
    my $unit;

    if (defined $size) {
	    if (Util::validDiskSize($size) and Util::getDiskSize($size, $sizeInKb, $unit))  {
            if ( $unit eq "T" || $unit eq "t" ) {
                $sizeInKb = $sizeInKb * 1024 * 1024 * 1024;
            } elsif ( $unit eq "G" || $unit eq "g" ) {
                $sizeInKb = $sizeInKb * 1024 * 1024;
            } elsif ( $unit eq "M" || $unit eq "m" ) {
                $sizeInKb = $sizeInKb * 1024;
            }
        } else {
            die new UsageErr("Unrecognized disk size: $size");
        }
    }
    my $host = currentHostSys($interp);
    VM::createScsiDisk($host, $vm, $store, $sizeInKb);
    return Interpreter::STATUS_OK;
}

sub cmd_vm_disk_destroy {
    my ($interp, $args) = @_;
    my $vmName = get_vm_name($interp, $args)
	and @$args == 1
	or die new UsageErr;
    my $vm = VM::find($vmName) or die noVmNamed($vmName);
    my $diskName = shift(@$args);
    VM::destroyScsiDisk($vm, $diskName);
    return Interpreter::STATUS_OK;
}

sub cmd_vm_disk_reassign {
    my ($interp, $args) = @_;
    my $srcVmName = get_vm_name($interp, $args)
	and @$args == 1 or die new UsageErr;
    my $srcVm = VM::find($srcVmName) or die noVmNamed($srcVmName);
    my $destVmName = get_vm_name($interp, $args)
	and @$args == 0 or die new UsageErr;
    my $destVm = VM::find($destVmName) or die noVmNamed($destVmName);
    VM::reassignDisks(currentHostSys($interp), $srcVm, $destVm);
    return Interpreter::STATUS_OK;
}

sub cmd_vm_serial_show {
    my ($interp, $args) = @_;
    my $vmName = get_vm_name($interp, $args)
	and @$args == 0
	or die new UsageErr;
    my $vm = VM::find($vmName) or die noVmNamed($vmName);
    VM::showSerial($vm);
    return Interpreter::STATUS_OK;
}

sub cmd_vm_serial_create {
    my ($interp, $args) = @_;
    my $vmName = get_vm_name($interp, $args)
	and @$args == 0
	or die new UsageErr;
    my $vm = VM::find($vmName) or die noVmNamed($vmName);
    VM::createSerial($vm);
    return Interpreter::STATUS_OK;
}

sub cmd_vm_serial_destroy {
    my ($interp, $args) = @_;
    my $vmName = get_vm_name($interp, $args)
	and @$args == 1
	or die new UsageErr;
    my $vm = VM::find($vmName) or die noVmNamed($vmName);
    VM::destroySerial($vm, $$args[0]);
    return Interpreter::STATUS_OK;
}

sub cmd_vm_prop_set {
    my ($interp, $args) = @_;
    my $vmName = get_vm_name($interp, $args)
	and @$args > 0
	or die new UsageErr;
    my $vm = VM::find($vmName) or die noVmNamed($vmName);
    VM::setProperty($vm, $args);
    return Interpreter::STATUS_OK;
}

sub cmd_vm_prop_unset {
    my ($interp, $args) = @_;
    my $vmName = get_vm_name($interp, $args)
	and @$args > 0
	or die new UsageErr;
    my $vm = VM::find($vmName) or die noVmNamed($vmName);
    VM::unsetProperty($vm, $args);
    return Interpreter::STATUS_OK;
}

sub cmd_vm_prop_show {
    my ($interp, $args) = @_;
    my $vmName = get_vm_name($interp, $args)
	and @$args == 0
	or die new UsageErr;
    my $vm = VM::find($vmName) or die noVmNamed($vmName);
    VM::showProperty($vm);
    return Interpreter::STATUS_OK;
}

sub vm_power($$$$) {
    my ($host, $vmName, $op, $force) = @_;
    my $vm = VM::find($vmName) or die noVmNamed($vmName);
    VM::power($host, $vm, $op, $force);
    return Interpreter::STATUS_OK;
}

sub cmd_vm_start {
    my ($interp, $args) = @_;
    my $force = !Config::isPromoted() && get_force_opt($args);
    my $vmName = get_vm_name($interp, $args)
	and @$args == 0
	or die new UsageErr;
    return vm_power(currentHostSys($interp), $vmName, "start", $force);
}

sub cmd_vm_stop {
    my ($interp, $args) = @_;
    my $force = get_force_opt($args);
    my $vmName = get_vm_name($interp, $args)
	and @$args == 0
	or die new UsageErr;
    return vm_power(currentHostSys($interp), $vmName, "stop", $force);
}

sub cmd_vm_restart {
    my ($interp, $args) = @_;
    my $force = get_force_opt($args);
    my $vm = get_vm_name($interp, $args)
	and @$args == 0
	or die new UsageErr;
    return vm_power(currentHostSys($interp), $vm, "restart", $force);
}

sub cmd_pool_show {
    my ($interp, $args) = @_;
    die new UsageErr unless (@$args == 0);
    POOL::show(currentHostSys($interp));
    return Interpreter::STATUS_OK;
}

sub cmd_pool_create {
    my ($interp, $args) = @_;
    die new UsageErr unless (@$args == 2);
    my $host = currentHostSys($interp);
    my $disk = PDISK::findUnused($host, $$args[1]);
    $disk or die new InvalidArg("<canonical_name>", "cannot use disk '$$args[1]'");
    POOL::create($host, $$args[0], $disk);
    return Interpreter::STATUS_OK;
}

sub cmd_pool_destroy {
    my ($interp, $args) = @_;
    die new UsageErr unless (@$args == 1);
    my $host = currentHostSys($interp);
    my $pool = POOL::find($host, $$args[0])
	or die new InvalidArg("<pool_name>", "no pool named '$$args[0]'");
    POOL::destroy($host, $pool);
    return Interpreter::STATUS_OK;
}

sub cmd_network_show {
    my ($interp, $args) = @_;
    die new UsageErr if (@$args != 0);
    NETWORK::show(currentHostSys($interp));
    return Interpreter::STATUS_OK;
}

sub cmd_pdisk_show {
    my ($interp, $args) = @_;
    die new UsageErr if (@$args != 0);
    PDISK::show(currentHostSys($interp));
    return Interpreter::STATUS_OK;
}

sub cmd_vm_monitor_show {
    my ($interp, $args) = @_;
    my $vmName = get_vm_name($interp, $args);
    @$args == 0 or die new UsageErr;
    verifyLocks();
    if ($vmName) {
	Monitor::show($vmName);
    } else {
	Monitor::list();
    }
    return Interpreter::STATUS_OK;
}

sub cmd_vm_monitor_log_show {
    my ($interp, $args) = @_;
    my $vmName = get_vm_name($interp, $args)
	or die new UsageErr;
    my $n = shift(@$args);
    @$args == 0 or die new UsageErr;
    my $logfile =  Monitor::monitorLogfile($vmName, $n);
    if (Util::shouldInject("vm_monitor_log_show_cmdfail")) {
	$n = Config::MAX_OLD_LOGS + 1;
    }
    if (defined $n && $n > Config::MAX_OLD_LOGS) {
	die new CmdFailed("$vmName monitor log $n does not exist");
    }
    if (-e $logfile) {
	system("/bin/cat $logfile");
    } elsif (! VM::find($vmName)) {
	die noVmNamed($vmName);
    } elsif (defined $n) {
	die new CmdFailed("$vmName monitor log $n does not exist");
    }
    return Interpreter::STATUS_OK;
}

sub cmd_vm_monitor_log_clear {
    my ($interp, $args) = @_;
    my $vmName = get_vm_name($interp, $args)
	and @$args == 0
	or die new UsageErr;
    my $injection = Util::injectionFor("vm_monitor_log_clear_cmdfail");
    die new CmdFailed($injection->msg) if $injection;
    Monitor::rotateLog($vmName)
	or VM::find($vmName)
	or die noVmNamed($vmName);
    return Interpreter::STATUS_OK;
}

sub cmd_vm_monitor_start {
    my ($interp, $args) = @_;

    my @opts;
    while (@$args > 0 && $$args[0] =~ /^--/) {
	push(@opts, shift(@$args));
    }
    my $vmName = get_vm_name($interp, $args)
	and @$args == 0
	or die new UsageErr;
    my $vm = VM::find($vmName) or die noVmNamed($vmName);
    my $host = currentHostSys($interp);
    Monitor::start($host, $vm, $server, $user, $password, @opts);
    Monitor::show($vmName);
    return Interpreter::STATUS_OK;
}

sub cmd_vm_monitor_stop {
    my ($interp, $args) = @_;
    my $force = get_force_opt($args);
    my $vmName = get_vm_name($interp, $args)
	and @$args == 0
	or die new UsageErr;
    Monitor::stop($vmName, $force);
    return Interpreter::STATUS_OK;
}

sub vmConsoleCleanup {
    if (defined $consoleVmName) {
	Util::unlock("console", $consoleVmName);
	undef $consoleVmName;
    }
}


sub vmConsoleDisconnect {
    my ($vmName, $force) = @_;
    my $lockfile = Util::lockfile("console.$vmName");
    if (open(LOCKFILE, "< " . $lockfile)) {
	my $pid = <LOCKFILE>;
	close(LOCKFILE);
	chomp $pid;
	kill($force ? "KILL" : "HUP", $pid);
    }
    if ($force) {
	unlink($lockfile);
    } else {
	# Wait a couple seconds for console to release lock(s)
	my $waitMs = 8;
	while ($waitMs <= 2048) {
	    last unless (-e $lockfile);
	    select(undef, undef, undef, $waitMs / 1000.0);
	    $waitMs *= 2;
	}
	if (-e $lockfile) {
	    die new VmConsoleNotStopping("'$vmName' console session did not end - may need --force");
	}
    }
    return Interpreter::STATUS_OK;
}

sub vmConsole($$;$) {
    my ($host, $vmName, $force) = @_;
    my $vm = VM::find($vmName)
	or die noVmNamed($vmName);

    if ($force) {
	eval {
	    vmConsoleDisconnect($vmName);
	};
	if ($@) {
	    vmConsoleDisconnect($vmName, $force);
	}
    }

    Util::lock("console", $vmName) or
	die new VmConsoleBusy("another console session is attached to '$vmName'");
    $consoleVmName = $vmName;	# for vmConsoleCleanup

    my $cons;
    eval {
	# Main console loop
	$cons = VM::serialSharedConn($host, $vm, VM::VM_CONSOLE_PORT, $debug);
	my $rin = "";
	vec($rin, fileno(STDIN), 1) = 1;
	vec($rin, fileno($cons), 1) = 1;
	input_mode_reset_flags(POSIX::ECHO | POSIX::ECHOK |
			       POSIX::ICANON | POSIX::ISIG);
	my $esc = $consoleEscapeKeyPrintable;
	Util::msg("Connecting to console on $vmName.  Use $esc-x to exit or $esc-h for help.\n");
	my $exitConsole;
	$SIG{"HUP"} = sub { $exitConsole = 1; };
	my ($escaped);
	while (1) {
	    my $rout = $rin;
	    select($rout, undef, undef, undef);
	    last if $exitConsole;
	    my ($buf, $bytes);
	    if (vec($rout, fileno($cons), 1)) {
		# Send console output to stdout
		$bytes = sysread($cons, $buf, 1024);
		if ($bytes) {
		    syswrite(STDOUT, $buf, $bytes);
		} else {
		    die "session-server disconnected";
		}
	    }
	    if (vec($rout, fileno(STDIN), 1)) {
		# Send keyboard input to console
		if (sysread(STDIN, $buf, 1)) {
		    if ($escaped) {
			if ($buf =~ /[xX]/) {
			    last;
			} elsif ($buf =~ /[hH\?]/) {
			    print "\n";
			    print "    x or X            exit console\n";
			    print "    h or H or ?       display this help text\n";
			    if ($consoleEscapeKey !~ /[xXhH\?]/) {
				my $esc = $consoleEscapeKeyPrintable;
				print "    $esc again          send $esc to console\n"
			    }
			    print "    anything else     sent to console\n";
			} else {
			    syswrite($cons, $buf, 1);
			    $escaped = 0;
			}
		    } elsif ($buf eq $consoleEscapeKey) {
			$escaped = 1;
		    } else {
			syswrite($cons, $buf, 1);
		    }
		}
	    }
	}
    };	# end of eval
    my $err = $@;
    delete $SIG{"HUP"};
    print STDERR "\n";
    input_mode_normal();
    close($cons) if defined $cons;

    vmConsoleCleanup();
    if ($err) {
	die $err->isa("Err") ? $err : new InternalErr($err);
    }
    return Interpreter::STATUS_OK;
}

sub cmd_vm_console_connect {
    my ($interp, $args) = @_;
    my $force = get_force_opt($args);
    my $vmName = get_vm_name($interp, $args)
	and @$args == 0
	or die new UsageErr;
    return vmConsole(currentHostSys($interp), $vmName, $force);
}


sub cmd_vm_console_disconnect {
    my ($interp, $args) = @_;
    my $force = get_force_opt($args);
    my $vmName = get_vm_name($interp, $args)
	and @$args == 0
	or die new UsageErr;
    return vmConsoleDisconnect($vmName, $force);
}


sub vmConsoleLog {
    my ($vmName, $n) = @_;
    my $logfile = Monitor::consoleLogfile($vmName, $n);
    my $monitorPid = Monitor::pid($vmName);
    if (-e $logfile) {
	system("/bin/cat $logfile");
	print STDERR "\n";
    } elsif (!VM::find($vmName)) {
	die noVmNamed($vmName);
    } elsif (defined $n) {
	die new CmdFailed("$vmName console log $n does not exist");
    }
    if (! defined $monitorPid) {
	Util::msg("\n[vm monitor for $vmName not currently running.");
	if (-e $logfile) {
	    my @stats = stat($logfile);
	    Util::msg("\n console log last written " . scalar localtime($stats[8]));
	}
	Util::msg("]\n");
    }
    return Interpreter::STATUS_OK;
}

sub cmd_vm_console_log_show {
    my ($interp, $args) = @_;
    my $vmName = get_vm_name($interp, $args)
	or die new UsageErr;
    my $n = shift(@$args);
    @$args == 0 or die new UsageErr;
    return vmConsoleLog($vmName, $n);
}

sub cmd_vm_console_log_clear {
    my ($interp, $args) = @_;
    my $vmName = get_vm_name($interp, $args)
	and @$args == 0
	or die new UsageErr;
    Monitor::rotateConsoleLog($vmName)
	or VM::find($vmName)
	or die noVmNamed($vmName);
    return Interpreter::STATUS_OK;
}


sub cmd_vm_ontap_config_backup {
    my ($interp, $args) = @_;
    my $vmName = get_vm_name($interp, $args)
	or die new UsageErr;
    my $name = shift(@$args);
    my $vm = VM::find($vmName)
	or die noVmNamed($vmName);
    my $host = currentHostSys($interp);
    VM::backupOntapSysDisks($host, $vm, $name);
    return Interpreter::STATUS_OK;
}


sub showSnapshots {
    my ($fmt, $vm, $snapshots) = @_;
    $snapshots = $vm->snapshot->rootSnapshotList unless $snapshots;
    foreach my $snapshot (@$snapshots) {
	my $name = $snapshot->name;
	printf($fmt, $name, $snapshot->createTime);
	if ($snapshot->childSnapshotList) {
	    showSnapshots($fmt, $vm, $snapshot->childSnapshotList);
	}
    }
}

sub cmd_vm_ontap_config_show {
    my ($interp, $args) = @_;
    my $vmName = get_vm_name($interp, $args)
	and @$args == 0
	or die new UsageErr;
    my $vm = VM::find($vmName)
	or die noVmNamed($vmName);
    if ($vm->snapshot) {
	my $fmt = "%-40s  %s\n";
	printf($fmt, "Name", "Created");
	showSnapshots($fmt, $vm);
    } else {
	Util::msg("There are no backups for $vmName\n");
    }
    return Interpreter::STATUS_OK;
}


sub cmd_vm_ontap_config_restore {
    my ($interp, $args) = @_;
    my $vmName = get_vm_name($interp, $args)
	and @$args < 2
	or die new UsageErr;
    my $backupName = shift(@$args);
    my $vm = VM::find($vmName)
	or die noVmNamed($vmName);
    my $host = currentHostSys($interp);
    VM::restoreOntapSysDisks($host, $vm, $backupName);
    return Interpreter::STATUS_OK;
}

sub cmd_vm_ontap_config_remove {
    my ($interp, $args) = @_;
    my $vmName = get_vm_name($interp, $args)
	and my $backupName = shift(@$args)
	and @$args == 0
	or die new UsageErr;
    my $vm = VM::find($vmName)
	or die noVmNamed($vmName);
    my $info = VM::findSnapshot($vm, $backupName)
	or die new InvalidArg("<backup_name>",
			      "No config backup named $backupName");
    my $snap = Vim::get_view(mo_ref => $info->snapshot);
    my $task = $snap->RemoveSnapshot_Task(removeChildren => 0);
    $task = Vim::get_view(mo_ref => $task);
    Util::waitForTask($task);
    return Interpreter::STATUS_OK;
}

sub cmd_vm_log_show {
    my ($interp, $args) = @_;
    my $vmName = get_vm_name($interp, $args)
	and @$args == 0
	or die new UsageErr;
    my $vm = VM::find($vmName)
	or die noVmNamed($vmName);

    # Use temporary log file
    my ($fh, $logfile) = File::Temp::tempfile($vmName . '-XXXXX',
					      DIR => Config::TMP_DIR_BASE,
					      SUFFIX => '.log',
					      UNLINK => 1);
    my $host = currentHostSys($interp);
    VM::log($host, $vm, $logfile, Util::injectionFor("vm_log_show_err"));

    # Print log file to screen
    system ("/bin/cat $logfile");
    unlink($logfile);

    return Interpreter::STATUS_OK;
}

sub cmd_vm_log_save {
    my ($interp, $args) = @_;
    my $vmName = get_vm_name($interp, $args)
	and @$args == 1
	or die new UsageErr;
    my $vm = VM::find($vmName)
	or die noVmNamed($vmName);
    my $host = currentHostSys($interp);
    VM::log($host, $vm, $$args[0], Util::injectionFor("vm_log_save_err"));
    return Interpreter::STATUS_OK;
}

sub cmd_rlm {
    my ($interp, $args) = @_;
    if (@$args == 1 && !isCommand($interp, $$args[0])) {
	my $vmName = shift(@$args);
	VM::find($vmName)
	    or die noVmNamed($vmName);
	$interp->setenv("vm_name", $vmName);
	$interp->setenv("prompt_extra", $vmName);
    } elsif (not $interp->getenv("vm_name")) {
	die new UsageErr;
    }
    return Interpreter::STATUS_OK;
}

sub cmd_rlm_system_power_on {
    my ($interp, $args) = @_;
    @$args == 0 or die new UsageErr;
    my $host = currentHostSys($interp);
    return vm_power($host, $interp->getenv("vm_name"), "start", 0);
}

sub cmd_rlm_system_power_off {
    my ($interp, $args) = @_;
    @$args == 0 or die new UsageErr;
    my $host = currentHostSys($interp);
    return vm_power($host, $interp->getenv("vm_name"), "stop", 1);
}

sub cmd_rlm_system_power_cycle {
    my ($interp, $args) = @_;
    @$args == 0 or die new UsageErr;
    my $host = currentHostSys($interp);
    return vm_power($host, $interp->getenv("vm_name"), "restart", 1);
}

sub cmd_rlm_system_console {
    my ($interp, $args) = @_;
    @$args == 0 or die new UsageErr;
    my $vmName = $interp->getenv("vm_name");
    my $host = currentHostSys($interp);
    return vmConsole($host, $vmName);
}

sub cmd_rlm_system_log {
    my ($interp, $args) = @_;
    @$args == 0 or die new UsageErr;
    my $vmName = $interp->getenv("vm_name");
    return vmConsoleLog($vmName);
}

sub cmd_asup_show {
    my ($interp, $args) = @_;
    @$args == 0 or die new UsageErr;
    ASUP::showConfig();
    return Interpreter::STATUS_OK;
}

sub cmd_asup_mailhost {
    my ($interp, $args) = @_;
    my %cfg;
    $cfg{"mailhost"} = shift(@$args) or die new UsageErr;
#    mailhost authentication not currently supported
#    $cfg{"mailhostUsername"} = @$args > 0 ? shift(@$args) : undef;
#    $cfg{"mailhostPassword"} = @$args > 0 ? shift(@$args) : undef;
    @$args == 0 or die new UsageErr;
    ASUP::config(%cfg);
    return Interpreter::STATUS_OK;
}

sub cmd_asup_recipients {
    my ($interp, $args) = @_;
    @$args > 0 or die new UsageErr;
    ASUP::config( recipients => join(",", @$args) );
    return Interpreter::STATUS_OK;
}

sub output_status($) {
    my ($interp) = @_;

    my $sdkVersion = $VMware::VIRuntime::VERSION;
    if (defined $sdkVersion) {
	Util::msg("VMware SDK version: $sdkVersion\n");
    }

    Util::msg("Console escape key: $consoleEscapeKeyPrintable\n");

    my $curHostname = $interp->getenv("hostname");
    return unless defined $curHostname;

    if ($apiType ne "HostAgent") {
	Util::msg("Host: $curHostname\n");
    }
}

sub cmd_status {
    my ($interp, $args) = @_;
    @$args == 0 or die new UsageErr;
    Util::msg("Version: " . Config::VERSION . "\n");
    Util::msg("Server: $server\n");
    Util::msg("User: $user\n");
    output_status($interp);
    return Interpreter::STATUS_OK;
}

sub cmd_support_show {
    my ($interp, $args) = @_;
    @$args == 0 or die new UsageErr;

    printf("dvadmin status:\n");
    cmd_status(@_);

    printf("\nAvailable networks:\n");
    cmd_network_show(@_);

    printf("\nAvailable physical disks:\n");
    cmd_pdisk_show(@_);

    printf("\nAvailable datastores:\n");
    cmd_pool_show(@_);

    printf("\nAvailable virtual machines:\n");
    VM::show(currentHostSys($interp, "vm"), undef, 1);

    return Interpreter::STATUS_OK;
}

sub cmd_history {
    my ($interp, $args) = @_;
    $term or die new CmdFailed("history only available in interactive sessions");
    print join("\n", $term->GetHistory()), "\n";
    return Interpreter::STATUS_OK;
}

sub cmd_source {
    my ($interp, $args) = @_;
    die new UsageErr if (@$args < 1 or @$args > 2);
    my $filename = shift(@$args);
    my $keep_going = ($filename eq "-k") || ($filename eq "--keep-going");
    $filename = shift(@$args) if ($keep_going);
    if (not open(SRCFILE, "< $filename")) {
	die new InvalidArg("<filename>", "cannot read file '$filename'");
    }
    my $stat = $interp->interpreter(\*SRCFILE, $keep_going);
    close(SRCFILE);
    return $stat;
}

sub cmd_help {
    my ($interp, $args) = @_;
    return $interp->help(\*STDOUT, 1, @$args);
}

sub cmd_exit {
    return Interpreter::STATUS_EXIT;
}

# "Shared" commands - valid at every level of cmd heirarchy
my $sharedCmds = {
    exit => {
	desc => "Exit interpreter",
	args => "",
	handler => \&cmd_exit },
    # TODO: "up" doesn't quite work right -- it goes back to the previous
    #	    command level rather than the one just above it.
    up => {
	desc => "Go up one level",
	args => "",
	handler => sub { return Interpreter::STATUS_UPLEVEL; }},
    ".." => {
	hidden => 1,		# don't include in general help
	desc => "Go up one level",
	args => "",
	handler => sub { return Interpreter::STATUS_UPLEVEL; }},
    help => {
	desc => "Show possible commands and usage details",
	args => "[command]",
	handler => \&cmd_help },
    "?" => {
	hidden => 1,
	desc => "Show possible commands and usage details",
	args => "[command]",
	handler => \&cmd_help },
    history => {
	desc => "Show command history",
	args => "",
	handler => \&cmd_history },
    source => {
	desc => "Execute commands from a file",
	args => "[-k/--keep-going] <filename>",
	handler => \&cmd_source },
    status => {
	desc => "Display dvadmin status info",
	args => "",
	handler => \&cmd_status },
};

# Longer help text
my $prop_set_help = join "\n",
    "Valid <prop_names> are:",
    "    license=<license>                    Data ONTAP-v platform license (20 chars)",
    "    hostname=<hostname>                  Data ONTAP-v Hostname",
    "    ipaddr=<ipaddr>                      Primary interface (e0a) IP address",
    "    netmask=<netmask>                    Primary interface (e0a) netmask",
    "    gateway=<gateway>                    Gateway for network connectivity",
    "    password=<passwd>                    Data ONTAP root password",
    "    checksum_type=<Checksum Type>        Checksum Type: 'block' or 'zoned' (RAID0 only)",
    "    vsphere_username=<user>              Username for read-only access to VM host",
    "    vsphere_password=<passwd>            Password for read-only access to VM host",
    "    tmz=<timezone>                       Time Zone",
    "    iface2_ipaddr=<ipaddr>               Second interface (e0b) ipaddr",
    "    iface2_netmask=<netmask>             Second interface (e0b) netmask",
    "    iface3_ipaddr=<ipaddr>               Third interface (e0c) ipaddr",
    "    iface3_netmask=<netmask>             Third interface (e0c) netmask",
    "    iface4_ipaddr=<ipaddr>               Fourth interface (e0d) ipaddr",
    "    iface4_netmask=<netmask>             Fourth interface (e0d) netmask",
    "    dns_domainname=<domainname>          DNS domain name",
    "    dns_ipaddr=<ipaddr1>,<ipaddr2>,..    Comma separated nameserver IP addresses",
    "    nis_domainname=<domainname>          NIS domain name",
    "    nis_ipaddr=<ipaddr1>,<ipaddr2>,..    Comma separated NIS IP addresses";

my $vm_create_help = join "\n",
    "[--memsize=<memory size>]     Size of memory; valid values are 4G, 32G, or 96G",
    "[--headswap]                  Create HeadSwap Data ONTAP-v virtual machine",
    "Required <props> for regular Data ONTAP-v virtual machine are",
    "    license=<license>         Data ONTAP-v platform license (20 chars)",
    "    hostname=<hostname>       Data ONTAP-v Hostname",
    "    ipaddr=<ipaddr>           Primary interface (e0a) IP address",
    "    netmask=<netmask>         Primary interface (e0a) netmask",
    "    gateway=<gateway>         Gateway for network connectivity",
    "    password=<passwd>         Data ONTAP root password",
    "    raidtype=<raidtype>       Raid Type: 'RAID0', 'RAID4' or 'RAID-DP'",
    "    vsphere_username=<user>   Username for read-only access to VM host",
    "    vsphere_password=<passwd> Password for read-only access to VM host",
    "Required <props> for HeadSwap Data ONTAP-v virtual machine are",
    "    license=<license>         Data ONTAP-v platform license (20 chars)",
    "    vsphere_username=<user>   Username for read-only access to VM host",
    "    vsphere_password=<passwd> Password for read-only access to VM host";

my $log_show_n_help = join "\n",
    "The [n] argument can be used to show older versions of the",
    "log file created by the 'log clear' commands.  The previous",
    "log is 1, the next oldest 2, etc.";

my $host_ssh_setup_help = join "\n",
    "If <ssh_password> is not specified, first tries",
    "vSphere password, then if that doesn't work prompts",
    "the user for the password if running interactively.",
    "The <ssh_password> is NOT stored.";

my $vm_coredump_help = join "\n",
    "Uses 'fast vm coredump', triggering Data ONTAP-v coredump",
    "instantaneously if non-interactive ssh connection has been",
    "set up (see 'status' output, and 'ssh setup' command).",
    "Otherwise falls back to VERY slow method of triggering",
    "coredump by suspending the VM then copying its memory image",
    "back and forth over the network, generally taking tens of",
    "minutes or even hours.  If the slow method is being used,",
    "the local directory in which to temporarily store the VM",
    "memory image can be specified by <temp_directory>.";

# Main command table defining heirarchy and driving implementation
my $mainCmds = {
    network => {
	desc => "Commands for managing networks",
	subcmds => {
	    show => {
		desc => "Show available networks",
		args => "",
		handler => \&cmd_network_show },
	}},
    pool => {
	desc => "Commands for managing storage pools",
	subcmds => {
	    show => {
		desc => "Show available storage pools",
		args => "",
		handler => \&cmd_pool_show },
	    create => {
		desc => "Create a VMFS storage pool",
                args => "<pool_name> <canonical_name>",
                handler => \&cmd_pool_create },
	    destroy => {
		desc => "Destroy a VMFS storage pool",
		args => "<pool_name>",
		handler => \&cmd_pool_destroy },
	}},
    pdisk => {
	desc => "Commands for managing physical storage",
	subcmds => {
	    show => {
		desc => "Show available physical disks",
		args => "",
		handler => \&cmd_pdisk_show },
	}},
    ssh => {
	desc => "Commands for managing ESX host SSH connection for fast vm coredumps",
	subcmds => {
	    setup => {
		desc => "Setup non-interactive SSH to ESX host",
		help => $host_ssh_setup_help,
		args => "[ssh_password]",
		handler => \&cmd_host_ssh_setup },
	    status => {
		desc => "Test non-interactive SSH to ESX host",
		args => "",
		handler => \&cmd_host_ssh_status },
	}},
    asup => {
	desc => "Commands for configuring NetApp autosupport in dvadmin",
	hidden => 1,
	subcmds => {
	    show => {
		desc => "Show current dvadmin autosupport configuration",
		args => "",
		handler => \&cmd_asup_show },
	    mailhost => {
		desc => "Set dvadmin autosupport mailhost",
		args => "<mailhost>", # [<username> <password>]",
		handler => \&cmd_asup_mailhost },
	    recipients => {
		desc => "Set dvadmin autosupport recipients",
		args => "<recipient_list>",
		handler => \&cmd_asup_recipients },
	}},
    support => {
	desc => "Support commands",
	subcmds => {
	    show => {
		desc => "Show system support information",
		args => "",
		handler => \&cmd_support_show },
	}},
    vm => {
	desc => "Commands for managing virtual machines",
	args => "[vm_name]",
	handler => \&cmd_vm,
	subcmds => {
	    show => {
		desc => "Display virtual machine details",
		args => "<vm_name>",
		handler => \&cmd_vm_show },
	    create => {
		desc => "Create Data ONTAP-v virtual machine.",
		args => "<vm_name> [--memsize=<memory size>] [--headswap] <pool_name> <nv_pool_name> <network_name> <ovf_pkg> <props>",
		help => $vm_create_help,
		handler => \&cmd_vm_create },
	    destroy => {
		desc => "Delete virtual machine",
		args => "<vm_name>",
		handler => \&cmd_vm_destroy },
	    start => {
		desc => "Start running a virtual machine",
		args => "<vm_name>",
		handler => \&cmd_vm_start },
	    stop => {
		desc => "Stop a running virtual machine",
		args => "[--force] <vm_name>",
		help => "Cleanly unless --force specified",
		handler => \&cmd_vm_stop },
	    restart => {
		desc => "Restart a running virtual machine",
                args => "[--force] <vm_name>",
		help => "Cleanly unless --force specified",
                handler => \&cmd_vm_restart },
	    gdb => {
		desc => "Control System Debug Access (GDB Connector)",
		not_promoted => 1,
		subcmds => {
		    start => {
			desc => "Start allowing GDB connections to a VM",
			args => "<vm_name> <tcp_port>",
			handler => \&cmd_vm_gdb_start },
		    show => {
			desc => "Show GDB connection information for a VM",
			args => "[vm_name]",
			handler => \&cmd_vm_gdb_show },
		    stop => {
			desc => "Stop allowing GDB connections to a VM",
			args => "<vm_name>",
			handler => \&cmd_vm_gdb_stop },
		}},
	    reservation => {
		desc => "Manage VM resource reservations",
		not_promoted => 1,	# not allowed by promoted builds
		subcmds => {
		    cpu => {
			desc => "Reserve CPU resources for VM",
			args => "<vm_name> [total_MHz]",
			handler => \&cmd_vm_reserve_cpu },
		    memory => {
			desc => "Reserve memory resources for VM",
			args => "<vm_name> [MBs]",
			handler => \&cmd_vm_reserve_memory },
		}},
	    htsharing => {
		desc => "Manage VM hyperthread sharing",
		not_promoted => 1,
		args => "<vm_name> <any, internal or none>",
		handler => \&cmd_vm_htsharing },
	    affinity => {
		desc => "Manage VM resource affinity setting",
		not_promoted => 1,
		subcmds => {
		    cpu => {
			desc => "Set VM vCPU affinity",
			args => "<vm_name> [list of logical CPUs]",
			handler => \&cmd_vm_affinity_cpu },
		    memory => {
			desc => "Set VM memory affinity",
			args => "<vm_name> [list of NUMA nodes]",
			handler => \&cmd_vm_affinity_memory },
		    show => {
			desc => "Display VM cpu and memory affinity setting and related host information",
			args => "[vm_name]",
			handler =>\&cmd_vm_affinity_show },
		}},
	    uuid => {
		desc => "Manage VM UUID",
		subcmds => {
		    show => {
			desc => "Display VM UUID",
			args => "<vm_name>",
			handler => \&cmd_vm_uuid_show },
		}},
	    log => {
		desc => "Access Data ONTAP-v system log file",
		subcmds => {
		    show => {
			desc => "Print the Data ONTAP-v system log",
			args => "<vm_name>",
			handler => \&cmd_vm_log_show },
		    save => {
			desc => "Save the Data ONTAP-v system log to a file",
			args => "<vm_name> <filename>",
			handler => \&cmd_vm_log_save },
		}},
	    coredump => {
		desc => "Cause the vm to dump core",
		args => "<vm name> [temp_directory]",
		help => $vm_coredump_help,
		handler => \&cmd_vm_coredump
		},
	    console => {
		desc => "Interact with the Data ONTAP-v system console",
		subcmds => {
		    connect => {
			desc => "Connect to the Data ONTAP-v system console",
			args => "[--force] <vm_name>",
			handler => \&cmd_vm_console_connect },
		    disconnect => {
			desc => "Disconnect another session from the Data ONTAP-v system console",
			args => "[--force] <vm_name>",
			handler => \&cmd_vm_console_disconnect },
		    log => {
			desc => "Manipulate Data ONTAP-v system console log file(s)",
			subcmds => {
			    show => {
				desc => "Print the Data ONTAP-v system console log",
				args => "<vm_name> [n]",
				help => $log_show_n_help,
				handler => \&cmd_vm_console_log_show },
			    clear => {
				desc => "Clear the current console log and rotate old logs",
				args => "<vm_name>",
				handler => \&cmd_vm_console_log_clear },
			},
		    },
		}},
	    network => {
		desc => "Virtual machine network command(s)",
		subcmds => {
		    show => {
			desc => "Display virtual machine network interfaces",
			args => "<vm_name>",
			handler => \&cmd_vm_network_show },
		    connect => {
			desc => "Connect virtual machine network interface",
			args => "<vm_name> <adapter_name> <network_name>",
			handler => \&cmd_vm_network_connect },
		}},
	    state => {
		desc => "Virtual machine state command(s)",
		subcmds => {
		    show => {
			desc => "Display virtual machine state",
			args => "<vm_name>",
			handler => \&cmd_vm_state_show },
		}},
	    config => {	# TODO: better name!  but "backup/restore" imply data too, to me
		desc => "Manipulate a Data ONTAP-v VM's ONTAP configuration",
		subcmds => {
		    backup => {
			desc => "Backup the VM's Data ONTAP configuration",
			args => "<vm_name> [backup_name]",
			handler => \&cmd_vm_ontap_config_backup },
		    remove => {
			desc => "Remove the named backup configuration",
			args => "<vm_name> <backup_name>",
			handler => \&cmd_vm_ontap_config_remove },
		    restore => {
			desc => "Restore the VM's Data ONTAP configuration",
			args => "<vm_name> [backup_name]",
			handler => \&cmd_vm_ontap_config_restore },
		    show => {
			desc => "Display backed up Data ONTAP configurations for VM",
			args => "<vm_name>",
			handler => \&cmd_vm_ontap_config_show },
		}},
	    monitor => {
		desc => "Control virtual machine monitor monitors",
		subcmds => {
		    show => {
			desc => "Show information about VM monitor(s)",
			args => "[vm_name]",
			handler => \&cmd_vm_monitor_show },
		    start => { desc => "Start a VM monitor",
			       args => "[--no-console-log] [--no-watchdog] [--watchdog-no-coredump] [--no-backup] <vm_name>",
			       handler => \&cmd_vm_monitor_start },
		    stop => { desc => "Stop a VM monitor",
			      args => "[--force] <vm_name>",
			      handler => \&cmd_vm_monitor_stop },
		    log => {
			desc => "Manipulate VM monitor logs",
			subcmds => {
			    show => {
				desc => "Display the log for a VM monitor",
				args => "<vm_name> [n]",
				help => $log_show_n_help,
				handler => \&cmd_vm_monitor_log_show },
			    clear => {
				desc => "Clear the current monitor log and rotate old logs",
				args => "<vm_name>",
				handler => \&cmd_vm_monitor_log_clear },
			}},
		}},
	    disk => {
		desc => "Manipulate virtual machine disks",
		subcmds => {
		    show => {
			desc => "Display virtual machine disks",
			args => "<vm_name>",
			handler => \&cmd_vm_disk_show },
		    create => {
			desc => "Create virtual machine disk",
			args => "<vm_name> <pool_name> [size <k|m|g|t>]",
			handler => \&cmd_vm_disk_create },
		    destroy => {
			desc => "Destroy virtual machine disk",
			args => "<vm_name> <scsiC:N>",
			handler => \&cmd_vm_disk_destroy },
		    reassign => {
			desc => "Reassign virtual machine disks",
			args => "<src_vm_name> <dest_vm_name>",
			handler => \&cmd_vm_disk_reassign },
		}},
	    serial => {
		desc => "Manipulate virtual machine serial ports",
		subcmds => {
		    show => {
			desc => "Display virtual machine serial ports",
			args => "<vm_name>",
			handler => \&cmd_vm_serial_show },
		    create => {
			not_promoted => 1,
			desc => "Create virtual machine serial port",
			args => "<vm_name>",
			handler => \&cmd_vm_serial_create },
		    destroy => {
			not_promoted => 1,
			desc => "Destroy virtual machine serial port",
			args => "<vm_name> <port>",
			handler => \&cmd_vm_serial_destroy },
		}},
	    prop => {
		desc => "Manipulate virtual machine properties",
		subcmds => {
		    show => {
			desc => "Display virtual machine properties",
			args => "<vm_name>",
			handler => \&cmd_vm_prop_show },
		    set => {
			desc => "Set virtual machine properties.",
			help => $prop_set_help,
			args => "<vm_name> [prop_name=val]+",
			handler => \&cmd_vm_prop_set },
		    unset => {
			desc => "Unset virtual machine properties",
			args => "<vm_name> [prop_name]+",
			handler => \&cmd_vm_prop_unset },
		}},
	    rlm => {
		desc => "Remote LAN Module synonyms",
		args => "<vm_name>",
		hidden => 1,
		handler => \&cmd_rlm,
		subcmds => {
		    system => {
			desc => "RLM system commands",
			subcmds => {
			    console => {
				desc => "Connect to the Data ONTAP-v system console",
				args => "",
				handler => \&cmd_rlm_system_console },
			    log => {
				desc => "Print the Data ONTAP-v system console log",
				args => "",
				handler => \&cmd_rlm_system_log },
			    power => {
				desc => "Commands for virtual power control",
				subcmds => {
				    on => {
					desc => "Start the Data ONTAP-v VM",
					args => "",
					handler => \&cmd_rlm_system_power_on },
				    off => {
					desc => "Forcibly power off the Data ONTAP-v VM",
					args => "",
					handler => \&cmd_rlm_system_power_off },
				    cycle => {
					desc => "Forcibly cycle Data ONTAP-v VM power off and on",
					args => "",
					handler => \&cmd_rlm_system_power_cycle },
				}},
			}},
		}},
		wizard => {
		  desc => "Start the VM create wizard",
		  handler => \&cmd_vm_wizard,		},
	}},
};


sub removeLeftoverLock($) {
    my ($lockname) = @_;
    if (Util::unlock($lockname)) {
	Util::warn("removed leftover lock $lockname\n");
    }
}


# Unclean shutdowns are inevitable in certain situations (like
# unexpected power outages), so we must be able to "clean up" certain
# state (the lockfiles) after them so that subsequent dvadmin sessions
# work correctly.
#
sub verifyLocks {

    # Make sure existing lockfiles are legitimate, removing any that aren't
    opendir(DIR, Config::LOCK_DIR) or return;

    while (my $filename = readdir(DIR)) {
	if ( $filename =~ /^monitor\..+$/ ) {
	    my ($ppid, $pid) = Util::lockinfo($filename);
	    # Remove monitor lock unless vm monitor is starting ...
	    unless ((! defined $pid && Util::isDvadminProcess($ppid)) ||
		    # ... or started ...
		    (defined $pid && Util::isDvadminProcess($pid))) {
		removeLeftoverLock($filename);
	    }
        } elsif ( $filename =~ /^console\..+$/ ) {
	    my ($pid) = Util::lockinfo($filename);
	    # Remove console lock unless console running ...
	    unless (Util::isDvadminProcess($pid)) {
		removeLeftoverLock($filename);
	    }
	}
    }

    close(DIR);
}


sub cleanup {
    if (defined $term) {
	# method may not exist
	eval { $term->WriteHistory(Config::CMD_HISTORY_FILE); };
    }
    if (defined $pterm) {
	input_mode_normal();
    }
    vmConsoleCleanup();
    VM::cleanup();
    if ($cred_initialized) {
	VMware::VICredStore::close();
    }
    if (defined $vim) {
	Util::msg("Logging out $user\@$server\n");
	$vim->logout();
	undef $vim;
    }
}

sub exitHandler {
    my ($why) = @_;
    Util::msg("Exiting due to $why\n");
    delete $SIG{"TERM"};
    cleanup();
    kill("TERM", $$);
}

# Set defaults from rc files
my @rcFiles = (Config::VSPHERE_CONFIG_FILE, Config::CONSOLE_CONFIG_FILE);
foreach my $rcFile (@rcFiles) {
    my $hostname;
    if (open(RCFILE, "< $rcFile")) {
	while (<RCFILE>) {
	    eval($_) || last;
	}
	close(RCFILE);
	if (defined $hostname) {
	    $hostnameInRcFile = $hostname;
	}
    }

}
my $savedServer = $server;

# Parse dvadmin cmdline
$Getopt::Long::order = $REQUIRE_ORDER;
GetOptions("s|server=s" => \$server,
	   "h|host=s" => \$hostnameInCmdLine,
	   "u|user=s" => \$user,
	   "p|password:s" => \$password,
	   "e|escape=s" => \$consoleEscapeKey,
	   "q|quiet" => \$quiet,
	   "d|debug=i" => \$debug,
	   "save" => \$save) ||
    usage();

# Validate args
usage("-s/--server must be specified") unless $server;
usage("-u/--user must be specified") unless $user;

# Parse and validate console escape key specification
if (defined $consoleEscapeKey) {
    if ($consoleEscapeKey =~ /^\^(.)$/) {
	# Interpret two-char key values starting with ^ as control keys.
	# Note $1 (from the =~ above) is the second char in the two-char value.
	my $ord1 = ord(uc($1));		# treat lowercase as upper
	if ($ord1 >= ord('@') && $ord1 <= ord('_')) {
	    $consoleEscapeKey = chr($ord1 - ord('@'));
	} elsif ($ord1 == ord('?')) {
	    $consoleEscapeKey = chr(127); # DEL
	} else {
	    usage("unrecognized control key given as console escape key");
	}
    }
    if (length($consoleEscapeKey) != 1) {
	usage("console escape key must be single character");
    }
    # Write rc file if requested
    if ($save) {
	open(RCFILE, "> " . Config::CONSOLE_CONFIG_FILE);
	print RCFILE '$consoleEscapeKey', " = chr(" . ord($consoleEscapeKey) . ")\n";
	close(RCFILE);
    }
}

# Set printable version of console escape key (for help)
$consoleEscapeKeyPrintable = $consoleEscapeKey;
if (ord($consoleEscapeKey) == 0) {
    $consoleEscapeKeyPrintable = '^@';
} elsif (ord($consoleEscapeKey) < ord(' ')) {
    $consoleEscapeKeyPrintable = '^' .
	chr(ord($consoleEscapeKey) - 1 + ord('A'));
} elsif (ord($consoleEscapeKey) == 127) {
    $consoleEscapeKeyPrintable = '^?';
}

# Set if running in quiet mode
if ($quiet) { Config::setQuiet(1); }
if ($debug) { Util::setDebugLevel($debug); }

# Initialize the credential store
eval {
    VMware::VICredStore::init(filename => Config::CRED_STORE_FILE);
    $cred_initialized = 1;
};
die "Cannot initialize VMware Credential Store\n$@" if ($@);

# Check if we should save the vsphere password
$save_vsphere_password = ( -e "/etc/dvadmin/no-saved-vsphere-password" ) ? 0 : 1;

# Get password if necessary
#
# Note if -p/--password is passed last w/o an arg, $password will be "" here,
#  so we won't look for it in the credential store, but we will prompt for it.
#
my $passwordStored;
if (not defined $password and $save_vsphere_password and -e Config::CRED_STORE_FILE) {
    # Look for password in VMware credential store
    $password = VMware::VICredStore::get_password(server => $server,
						  username => $user);
    $passwordStored = $password;
}
if (not $password) {
    $password = getpass("Password for vSphere user $user\@$server: ");
}

# Print banner
Util::msg(BANNER);

# Clean up after unclean shutdown (kill -9 or power outage)
verifyLocks();

# Login to vSphere server

# timeout flag
my $server_login_timeout = 0;

# timeout length in second
my $timeout = Config::SERVER_LOGIN_TIMEOUT;

sub server_login_die_handler {
    if ("@_" =~ /abc_SERVER_LOGIN_TIMEOUT_xyz/) {
	$server_login_timeout = 99999;
    }
}

Util::msg("Logging in $user\@$server\n");
eval {
    # die-handler since login can overwrite the die message
    local $SIG{__DIE__} = \&server_login_die_handler;
    local $SIG{ALRM} = sub { die "abc_SERVER_LOGIN_TIMEOUT_xyz"; };

    # start timer
    alarm $timeout;

    $vim = Vim::login(service_url => "https://$server/sdk",
		      user_name => $user, password => $password);

    # cancel timer
    alarm 0;
};

if ($@) {
    my $err;

    if ($server_login_timeout == 99999) {
	$err = new ConnectionTimeout($server, $timeout);
    } elsif ( "$@" =~ /Bad hostname/ ) {
	$err = new HostNotFound($server);
    } elsif ( "$@" =~ /Connection refused/ ) {
	$err = new ConnectionRefused($server);
    } else {
	$err = new LoginFailed($server, $user);
    }
    Util::error("$err\n");
    exit $err->code;
}

# Save password (now that we've verified it) in credential store.
if ($save_vsphere_password) {
    if (not $passwordStored) {
	VMware::VICredStore::add_password(server => $server,
					  username => $user,
					  password => $password);
    }
}

# Get host info
my $initialHostname;
$apiType = Vim::get_service_content()->about->apiType;
if ($apiType eq "HostAgent") {

    # Talking to ESX host
    $initialHostname = $server;

    # Servername (-s) must be a vCenter server if hostname(-h)
    # is provided.
    usage("-s/--server must be a vCenter server when -h/--host is specified.\n") if defined $hostnameInCmdLine;

    # Get this started now to overlap host info fetching
    Host::sshStatusCheckStart($initialHostname, $password);

    my $hostSum = Host::get($server, "summary")
	or die "Host::get($server, 'summary') failed\n";

    # Warn user if this is a managed host i.e. under control of vCenter server
    # managementServerIp will be set to an IP address for managed hosts. check
    # if this value is set and convert it to a hostname if possible
    my $mgmtServerIp = $hostSum->summary->managementServerIp;
    my $mgmtServer = gethostbyaddr(inet_aton($mgmtServerIp), AF_INET) if ($mgmtServerIp);
    $mgmtServer = $mgmtServerIp unless ($mgmtServer);
    if ($mgmtServer) {
	Util::warn("'$server' is a managed host. Please connect via the vCenter server ($mgmtServer).\n");
    }

} else {

    # Talking to vCenter server
    $initialHostname = $hostnameInCmdLine || $hostnameInRcFile;
    usage("-h/--host must be specified since $server is $apiType\n") unless defined $initialHostname;

    # Get this started now to overlap host info fetching
    Host::sshStatusCheckStart($initialHostname, $password);

    usage("invalid hostname '$initialHostname'.") unless Host::isValidHost($initialHostname);
}

# Write rc file if requested
if ($save) {
    open(RCFILE, "> " . Config::VSPHERE_CONFIG_FILE);
    print RCFILE '$server', " = '$server'\n";
    print RCFILE '$user', " = '$user'\n";
    print RCFILE '$hostname', " = '$initialHostname'\n" if defined $initialHostname;
    close(RCFILE);
}

sub is_interactive() {
    # Not interactive if output is not to terminal...
    return 0 if not -t *STDOUT;

    # If *ARGV isn't opened, it will be interactive if *STDIN is attached
    # to a terminal and either there are no files specified on the command line
    # or if we are invoking it to do something and exit.
    return -t *STDIN if (@ARGV==0 || $ARGV[0] eq 'exit');

    return 1;
}

if (is_interactive()) {
    $term = Term::ReadLine->new("dvadmin");
    my $ornaments = $ENV{"DVADMIN_PROMPT_ORNAMENTS"} || ",,,";
    $term->Attribs->ornaments($ornaments);

    # Limit line history, if possible (method may not exist)
    eval { $term->StifleHistory(Config::CMD_HISTORY_COUNT); };

    # Read history from history file, if possible (method may not exist)
    eval { $term->ReadHistory(Config::CMD_HISTORY_FILE); };
}

# Used for refreshHandlerEnv fault injection; see below.
my $refreshes = 0;

sub refreshHandlerEnv {
    # The interpreter will call this before interpreting each command.  It
    # is responsible for ensuring the interpretation environment (in this
    # case, the connection to vSphere and the host cache) is "ready" to
    # be used by command handlers.

    # Special case: no need to refresh if we're exiting
    my ($entry) = @_;
    return if ($$entry{"handler"} == \&cmd_exit);

    # Fault injection: refresh_err fault simulates session timeout and host
    # lookup failure every other refresh attempt for testing purposes.
    my $injectFault = Util::shouldInject("refresh_err") && ($refreshes++ & 1) == 0;

    my $current = eval {
	! $injectFault &&
	    Vim::get_view(mo_ref => Vim::get_service_content()->sessionManager)->currentSession
    };
    if ($@ || ! $current) {
	Util::dmsg(1, "$user\@$server session expired - refreshing\n");
	Host::invalidateHostCache();
	$vim = Vim::login(service_url => "https://$server/sdk",
			  user_name => $user, password => $password);

	die new InternalErr("host can no longer be found") if $injectFault;
    }
}

my $interp = Interpreter->new("dvadmin", $mainCmds, $sharedCmds, \*STDIN, $term, \&refreshHandlerEnv);
$interp->setenv("hostname",  $initialHostname);
output_status($interp) if defined $term;

# Register exit signal handler
foreach my $sig (qw(TERM TRAP ABRT BUS FPE USR1 USR2 ALRM PIPE)) {
    $SIG{$sig} = \&exitHandler;
}

# Ignore SIGINT from keyboard.  Better handled w/Termios??
$SIG{"INT"} = 'IGNORE';


# Process command
my $stat;
if (@ARGV == 0) {
    # No args - drop immediately into interpreter
    $stat = $interp->interpreter();
} elsif (@ARGV == 1) {
    # One arg - parse and interpret
    $stat = $interp->interpret($interp->parse($ARGV[0]));
} else {
    # Multiple args - interpret without further parsing
    $stat = $interp->interpret(@ARGV);
}
$stat = Interpreter::STATUS_OK if ($stat == Interpreter::STATUS_EXIT);
exit $stat;

END {
    cleanup();
}
